package utils

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"github.com/hpcloud/tail"
	"github.com/sevlyar/go-daemon"
	"github.com/shirou/gopsutil/process"
	"github.com/tidwall/gjson"
	"github.com/vmihailenco/msgpack/v5"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

type Hotfix struct {
	Title string `yaml:"title" json:"title"`
	// 源代码编译目录
	SrcDir string `yaml:"src" json:"src"`
	// 目标运行目录
	DestDir string `yaml:"dest" json:"dest"`
	// 编译文件名
	ExecOrigin string `yaml:"origin" json:"origin"`
	// 运行文件名
	ExecName string `yaml:"name" json:"name"`
	// 运行参数
	LaunchArg string `yaml:"arg" json:"arg"`
	// 末行输出分钟数(tail -f)
	WaitMinutes time.Duration `yaml:"wait" json:"wait"`
	// 自动更新执行时间
	CronSeconds time.Duration `yaml:"-" json:"-"`
	// 自动更新执行时间(换算后仅用于显示)
	CronTime string `yaml:"-" json:"time"`
	// 子游戏所在目录(通过命令函数赋值)
	CronGame string `yaml:"-" json:"game"`
	// 钉钉群通知WEB钩子
	WebHook string `yaml:"hook" json:"hook"`
	// 链接库文件(源)名
	SrcLibList []string `yaml:"libs" json:"libs"`
	// 可执行/链接库文件(源)哈希值
	SrcMd5List []string `yaml:"-" json:"md5s"`
	// 运行的可执行文件日志目录
	GameLogDir string `yaml:"-" json:"log"`
	// 运行的可执行文件游戏类型
	GameType string `yaml:"-" json:"type"`
	// 守护进程上下文
	*daemon.Context `yaml:"-" json:"-"`
}

// 初始化自动更新参数
func (that *Hotfix) Init() string {
	if len(that.CronGame) == 0 || len(that.CronTime) == 0 {
		flag.Usage()
		os.Exit(0)
	}

	// 相对路径转绝对路径
	that.SrcDir, _ = filepath.Abs(that.SrcDir)
	that.DestDir, _ = filepath.Abs(that.DestDir)
	// 打完所有局1分钟后更新
	if that.WaitMinutes < 1 {
		that.WaitMinutes = 1
	}

	filename := filepath.Join("list", that.CronGame+".yaml")
	buf, err := ioutil.ReadFile(filename)
	if err != nil {
		log.Fatalln(">>> 读取自定义配置文件失败 ->", err)
	}

	if err := yaml.Unmarshal(buf, struct {
		Hotfix *Hotfix `yaml:"hotfix"`
	}{that}); err != nil {
		log.Fatalln(">>> 解析自定义配置文件失败 ->", err)
	}

	CFBDecryptByRef(&that.WebHook)

	// 比较源可执行文件与目标的HASH值
	if that.CmpMd5() {
		log.Fatalln(">>> 子游戏", that.CronGame, "已是最新版本!!!")
	}

	// 读取可执行文件JSON参数
	file := filepath.Join(that.DestDir, that.LaunchArg)
	if jsons, err := ioutil.ReadFile(file); err != nil {
		log.Fatalln(">>> 读取游戏配置文件失败 ->", err)
	} else {
		result := gjson.ParseBytes(jsons)
		that.GameLogDir = result.Get("glog_path").String()
		that.GameType = result.Get("server_type").String()
		if len(that.GameLogDir) == 0 || len(that.GameType) == 0 {
			log.Fatalln(">>> 解析游戏配置文件失败 ->", err)
		}
	}

	if filepath.IsAbs(that.GameLogDir) {
		that.GameLogDir = filepath.Join(that.GameLogDir, that.ExecName+".INFO")
	} else {
		that.GameLogDir = filepath.Join(that.DestDir, that.GameLogDir, that.ExecName+".INFO")
	}

	buf, err = msgpack.Marshal(that)
	if err != nil {
		log.Fatalln(">>> 序列化守护进程参数失败 ->", err)
	}
	// 倒计时执行"热更新"
	display := *that // 深拷贝
	display.WebHook = "不打印敏感信息"
	if buf, err := json.MarshalIndent(display, "", "  "); err == nil {
		log.Println(">>> 自动更新配置参数(Init):", string(buf))
	}

	return base64.StdEncoding.EncodeToString(buf)
}

// 比较文件的MD5哈希值
func (that *Hotfix) CmpMd5() bool {
	filename := filepath.Join("md5", that.CronGame+".txt")
	buf, _ := ioutil.ReadFile(filename)

	srcMd5 := FileMd5(filepath.Join(that.SrcDir, that.ExecOrigin))
	that.SrcMd5List = append(that.SrcMd5List, srcMd5)
	for _, v := range that.SrcLibList {
		if PathExist(filepath.Join(that.SrcDir, v)) {
			srcMd5 := FileMd5(filepath.Join(that.SrcDir, v))
			that.SrcMd5List = append(that.SrcMd5List, srcMd5)
		}
	}

	oldMd5List := strings.Split(string(buf), ",")
	oldMd5List = deleteEmpty(oldMd5List)
	if len(that.SrcMd5List) != len(oldMd5List) {
		return false
	}

	for i := 0; i < len(that.SrcMd5List); i++ {
		if that.SrcMd5List[i] != oldMd5List[i] {
			return false
		}
	}

	return true
}

// 把新的MD5哈希值写入文件
func (that *Hotfix) LogMd5() bool {
	filename := filepath.Join("md5", that.CronGame+".txt")
	str := strings.Join(that.SrcMd5List, ",")
	if err := ioutil.WriteFile(filename, []byte(str), 0644); err != nil {
		log.Println(">>> 写入新的MD5值到", filename, "失败 ->", err)
		return false
	}
	return true
}

type Message struct {
	Type string `json:"msgtype"`
	Text struct {
		Content string `json:"content"`
	} `json:"text"`
}

func (that *Hotfix) Report(a ...interface{}) {
	content := fmt.Sprintln(a...)
	content = content[:len(content)-1] // 删除最后一个字符(回车)
	msg := Message{Type: "text"}
	msg.Text.Content = fmt.Sprintln("Hotfix >>>", that.ExecName, ":", content)
	log.Println(msg.Text.Content)

	buf, _ := json.Marshal(msg)
	request, err := http.NewRequest("POST", that.WebHook, bytes.NewBuffer(buf))
	if err != nil {
		_ = that.Context.Release()
		log.Fatalln("创建", msg.Text.Content, "请求失败:", err)
	}
	request.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	response, err := client.Do(request)
	if err != nil {
		_ = that.Context.Release()
		log.Fatalln("发送", msg.Text.Content, "请求失败:", err)
	}
	defer func() { _ = response.Body.Close() }()

	_ = that.Context.Release()
	os.Exit(0)
}

func (that *Hotfix) Run(mysql *Mysql, b64str string) {
	buf, _ := base64.StdEncoding.DecodeString(b64str)
	if err := msgpack.Unmarshal(buf, that); err != nil {
		that.Report(">>> 反序列化守护进程参数失败 ->", err)
	}

	display := *that // 深拷贝
	display.WebHook = "不打印敏感信息"
	if buf, err := json.MarshalIndent(display, "", "  "); err == nil {
		log.Println(">>> 自动更新配置参数(Run):", string(buf))
	}

	if _, err := os.Stat(that.GameLogDir); os.IsNotExist(err) {
		that.Report("要监视的日志文件不存在:", that.GameLogDir)
	}

	timer := time.NewTimer(time.Second * that.CronSeconds)
	<-timer.C // 等待倒计时结束

	if err := mysql.Open(); err != nil {
		that.Report("打开MySQL数据库失败:", err)
	}

	defer func() {
		_ = mysql.Close()
	}()

	// 设置茶馆为关闭状态
	if err := mysql.Closed(that.GameType); err != nil {
		that.Report("关闭游戏ID为", that.GameType, "的茶馆失败:", err)
	}

	watch, err := tail.TailFile(that.GameLogDir, tail.Config{Follow: true})
	if err != nil {
		that.Report("打开监视日志", that.GameLogDir, "失败:", err)
	}

	log.Println("开始监视日志", that.GameLogDir)

	// 等待茶馆所有桌打完
	timer.Reset(time.Minute * that.WaitMinutes)

	go func() {
		select {
		case <-timer.C:
			log.Println("准备停止监视日志...")
			_ = watch.Stop()
		}
	}()

	for range watch.Lines {
		timer.Reset(time.Minute * that.WaitMinutes)
	}

	// 杀死目标可执行文件
	ps, _ := process.Processes()
	for _, v := range ps {
		name, _ := v.Name()
		if name == that.ExecName {
			log.Println("开始执行杀死进程:", name)
			if err := v.Kill(); err != nil {
				that.Report("执行杀死进程失败:", err)
			}
		}
	}

	time.Sleep(time.Second * 1)

	src := filepath.Join(that.SrcDir, that.ExecOrigin)
	dest := filepath.Join(that.DestDir, that.ExecName)
	// 由于动态链接库路径为源目录(绝对路径)下故无需拷贝它们
	if err := CopyFile(src, dest); err != nil {
		that.Report("从", src, "拷贝到", dest, "失败:", err)
	}

	// 新的目标文件无需校验哈希值
	if that.LogMd5() {
		log.Println("从", src, "拷贝到", dest, "成功!!!")
	}

	if err := os.Chmod(dest, 0755); err != nil {
		that.Report("变更文件", dest, "权限失败:", err)
	}

	// 启动目标可执行文件
	cmd := exec.Command("./"+that.ExecName, that.LaunchArg)
	cmd.Dir = that.DestDir
	if err := cmd.Run(); err != nil {
		that.Report("在目录", cmd.Dir, "运行 [", strings.Join(cmd.Args, " "), "] 失败:", err)
	}

	log.Println("在目录", cmd.Dir, "运行 [", strings.Join(cmd.Args, " "), "] 成功!!!")

	// 设置茶馆为开启状态
	if err := mysql.Opened(that.GameType); err != nil {
		that.Report("打开游戏ID为", that.GameType, "的茶馆失败:", err)
	}
	that.Report(that.Title, "*** 自动更新成功完成:", time.Now().Format(LayoutChangsha))
}
